Skip to content

feat(auth): SEP-10-style challenge-transaction signature scheme - #138

Merged
EmeditWeb merged 1 commit into
mainfrom
feat/auth-challenge-transaction
Sep 29, 2026
Merged

EmeditWeb merged 1 commit into
mainfrom
feat/auth-challenge-transaction

Conversation

@EmeditWeb

Copy link
Copy Markdown
Member

🔗 Related Issue

No API-side issue. This unblocks real wallet-signature JWT auth in the mobile app (a follow-up StepFi-App PR wires the client flow and closes the app issue). This PR is the API prerequisite it depends on.


🔖 Title

Add a sep0010 challenge-transaction signature scheme to POST /auth/verify.


📝 Description

POST /auth/verify previously only accepted an Ed25519 signature over message bytes (raw / sep0043 / envelope). Mobile Lobstr over WalletConnect only exposes stellar_signXDR — it cannot sign arbitrary messages — so mobile clients could not produce a real signature and were stuck on mock tokens.

This adds a SEP-10-style scheme: the wallet signs a server-issued challenge transaction (which both Lobstr and Freighter already support) instead of a message.

POST /auth/nonce now also returns challengeXdr: an unsigned challenge transaction whose source is the user's own wallet at sequence 0 (built on an Account seeded at "-1"), carrying a single manageData operation whose value equals the SHA-256 hash already stored on the nonce row. The client signs this XDR and submits it back as signedXdr with signatureType: "sep0010".

Verification asserts the signed transaction is the challenge we issued:

  • source === wallet, sequence === "0"
  • exactly one manageData op with the expected name
  • op value bytes === the stored challenge hash (the binding)
  • timebounds not expired
  • a signature over tx.hash() that verifies against the wallet key (the network passphrase is bound implicitly, since the hash only matches for the correct network)

Deliberate deviation from strict SEP-10: the challenge is server-issued but not server-signed, so no new server SIGNING_KEY secret is introduced. Forgery/replay is already prevented by the existing single-use nonce row (atomic claim), the stored message_hash binding, and the timebounds. A .build()ed transaction with a source account at sequence 0 can never be submitted to the network, so it is a pure auth artifact. Strict SEP-10 (server co-signature + WEB_AUTH_DOMAIN) remains a documented follow-up if ever required.

The legacy raw / sep0043 / envelope message schemes are unchanged.


🔄 Changes Made

  • auth.service: build the unsigned challenge tx in generateNonce; verify the signed challenge in a new sep0010 branch of verifySignature (reuses AUTH_SIGNATURE_INVALID / AUTH_CHALLENGE_MISMATCH / AUTH_NONCE_EXPIRED)
  • nonce-response.dto: add challengeXdr
  • verify-request.dto: add 'sep0010' to signatureType; add optional signedXdr; make signature conditional (not required for sep0010)
  • Tests: new real-SDK round-trip spec (happy path + tampered value, wrong source, wrong op name, expired timebounds, unsigned, malformed XDR, consumed nonce); extend the existing mocked spec

🗒️ Additional Notes

  • Verified locally: npm run build, npm run lint:ci (0 warnings), npm test (485 passing).
  • The new sep0010 spec deliberately opts out of the repo's manual test/__mocks__/stellar-sdk.js (via jest.unmock) so it exercises the genuine sign→verify round-trip with a throwaway Keypair — the end-to-end proof that a signXDR-only wallet can now authenticate.

Add a 'sep0010' verification scheme to POST /auth/verify so wallets that
only expose transaction signing (mobile Lobstr over WalletConnect
stellar_signXDR) can authenticate, not just wallets that sign arbitrary
messages.

POST /auth/nonce now also returns `challengeXdr`: an unsigned SEP-10-style
challenge transaction whose source is the user's own wallet at sequence 0
(built on an Account seeded at "-1") with a single manageData operation
whose value equals the SHA-256 hash already stored on the nonce row. The
wallet signs this XDR and returns it as `signedXdr` with
`signatureType: 'sep0010'`.

Verification asserts the signed transaction is exactly the issued
challenge — matching source, sequence 0, single manageData op with the
expected name and a value equal to the stored challenge hash, non-expired
timebounds — and carries a valid wallet signature over the transaction
hash (the network passphrase is bound implicitly through tx.hash()).

Deliberate deviation from strict SEP-10: the challenge is server-issued
but not server-signed, so no server SIGNING_KEY secret is introduced.
Forgery/replay is already prevented by the existing single-use nonce row,
the stored message_hash binding, and the timebounds. Strict SEP-10
(server co-signature + WEB_AUTH_DOMAIN) remains a documented follow-up.

The legacy raw/sep0043/envelope message schemes are unchanged.

- nonce-response.dto: add challengeXdr
- verify-request.dto: add 'sep0010' to signatureType; add optional signedXdr;
  make signature conditional (not required for sep0010)
- tests: real-SDK round-trip spec (happy path + tamper/expiry/unsigned/
  wrong-source/wrong-name/consumed-nonce), extend existing mock
@EmeditWeb
EmeditWeb merged commit ff1d711 into main Sep 29, 2026
2 checks passed
@EmeditWeb
EmeditWeb deleted the feat/auth-challenge-transaction branch September 29, 2026 14:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant